module("ufe.ufe_module",package.seeall)

local conf = require("ufe.config")
local shared = require("ufe.util.shared")
local cjson = require("cjson.safe")
local u = require("ufe.util.update")

local CHECK_CONF_EXPIRED = conf.check_conf_expired or 30
local CHECK_SWITCH_EXPIRED = conf.check_switch_expired or 3
local LOAD_APP_CONF_RETRY_TIME = conf.load_app_conf_retry_time or 5

updating_conf = false
local pg_types={
    beta="beta",mini="mini",gray="gray"
}

local action_handlers={}
local value_handlers = {}

local ufe_confs = {last_check_time = 0}
local switch = {last_check_time = 0}

function register_policy(name, handler)
    handler.name = name
    action_handlers[name] = handler
end

function register_value(name, handler)
    handler.name = name
    value_handlers[name] = handler
end

function get_value_handler(t)
    return value_handlers[t]
end

local function sort_by_seq(t)
    local seqs = {}
    local new = {}
    for seq in pairs(t) do
        seqs[#seqs + 1] = seq
    end
    table.sort(seqs)

    for i, seq in ipairs(seqs) do
        new[i] = t[seq]
    end
    return new
end

function load_conf_fail(app, err)
    if app == nil then
        app = "ufe_apps"
        shared.set_ufe_err(err)
    else
        shared.set_app_err(app, err)
    end
    ngx.log(ngx.ERR, string.format("loading app %s failed: %s.", app, err))
end

local function check_ufe_apps(apps_json)
    if type(apps_json) ~= 'table' then
        return false, "wrong json array"
    end

    local apps = apps_json.ufe_apps
    if not apps then
        return false,"can not find ufe_apps"
    end

    if type(apps) ~= 'table' then
        return false, "apps is not an array"
    end

    for _,value in ipairs(apps) do
        if type(value) ~= 'string' then
            return false, "invalid apps"
        end
    end

    return true
end

local function check_ufe_app_conf(app, acf)
    if type(acf) ~= 'table' then
        return false, "wrong json format"
    end
    if acf.name ~= app then
        return false,"wrong app name"
    end

    if type(acf.ufe_id) ~= 'number' then
        return false, "not find ufe_id or ufe_id must be a number"
    end

    --if no policy
    if not acf.policy_groups then
        return false, "no policy_groups in "..app.." conf"
    end
    if type(acf.policy_groups) ~= 'table' then
        return false, "wrong policy_groups format in "..app.." conf"
    end
    if #acf.policy_groups == 0 then
        return false, "empty policy_groups in "..app.." conf"
    end

    for _, pg in pairs(acf.policy_groups) do
        if type(pg) ~= 'table' then
            return false, "wrong policy_group format in "..app.." conf"
        end

        if type(pg.seq) ~= 'number' then
            return false, "not find seq or seq must be a number"

        end
        if not pg_types[pg.type] then
            return false, "not find or support type in "..app.." conf"
        end

        if type(pg.pg_id) ~= 'number' then
            return false, "not find id or id must be a number"
        end

        if pg.unit_sign then
            if type(pg.unit_sign) ~= 'table' then
                return false, "wrong unit_sign list"
            end
            for _,u in ipairs(pg.unit_sign) do
                if type(u) ~= 'string' then
                    return false, "invalid unit_sign:"..tostring(u)
                end
            end
        end

        if pg.domains then --check domain
            if type(pg.domains) ~= 'table' then
                return false, "wrong domains list"
            end
            for _,domain in ipairs(pg.domains) do
                if type(domain) ~= 'string' then
                    return false, "invalid domain:"..tostring(domain)
                end
            end
        end

        if pg.uri then --check uri
            if type(pg.uri) ~= 'table' then
                return false, "wrong uri list"
            end
            for _,uri in ipairs(pg.uri) do
                if type(uri) ~= 'string' then
                    return false, "invalid uri:"..tostring(uri)
                end
            end

        end

    end

    return true
end

local function check_policy_conf(pcf)
    if type(pcf) ~= 'table' then
        return false,  "wrong policy format"
    end
    if type(pcf.seq) ~= 'number' then
        return false, "not found seq or seq must be a nubmer in policy"
    end

    if type(pcf.id) ~= 'number' then
        return false, "not found id or id must be a nubmer in policy"
    end

    if type(pcf.action) ~= 'table' then
        return false, "not found or wrong policy action format"
    end

    local action = pcf.action.name
    if not action_handlers[action] then
        return false, "not support policy action"
    end

    --check every policy
    return action_handlers[action].check(pcf.action)

end

local function check_pg_conf(pg_id, pgcf)
    if type(pgcf) ~= 'table' then
        return false, "wrong policy group format"
    end

    if pg_id ~= pgcf.id then
        return false, "policy group id is wrong"
    end

    if type(pgcf.policies) ~= 'table' then
        return false, "wrong policies format"
    end

    local policies = {}
    for _, policy in ipairs(pgcf.policies) do
        local ok, err = check_policy_conf(policy)
        if not ok then
            return false, err
        end
    end
    return true
end

local function update_apps_from_json(apps_str)
    local apps, err = cjson.decode(apps_str)
    if not apps then
        err = err or "unknown error"
        return false, "decode json failed:"..apps_str..".Error is:"..err
    end

    local succ, err = check_ufe_apps(apps)
    if not succ then
        err = err or "unknown error"
        return false, "wrong apps:"..err.." in "..apps_str
    end

    --set switch status unconditional, all invalid switch is off
    if  apps.ufe_switch == 'on' then
        shared.set_ufe_switch_locked(true)
    else
        shared.set_ufe_switch_locked(false)
    end

    --set update time to indicate whether the apps changed
    apps.update_time = ngx.time()
    apps_str = cjson.encode(apps)

    local ok, err = shared.set_ufe_apps(apps_str)
    if not ok then
        err = err or ""
        return false, "can not set apps in shared memory:"..err
    end
    return true
end

--get apps from remote
function get_apps_from_remote()
    local apps_str = u.get("ufe_tengine")
    if not apps_str then
        return  false, "can not read apps_str from remote"
    end
    ngx.log(ngx.INFO,"get apps :",apps_str)

    return update_apps_from_json(apps_str)
end

local function load_pg_conf(pg)
    local key = string.format("ufe_group_%d", pg.pg_id)
    local pgcf_str = u.get(key)

    if not pgcf_str then
        return nil, "can not get pg conf from remote"
    end

    local pgcf, err = cjson.decode(pgcf_str)
    if not pgcf then
        err = err or "unknown error"
        return nil, string.format("loading pg(id:%d):decode json failed:%s.\
            raw json:%s",pg.pg_id,err,pgcf_str)
    end

    local ok, err = check_pg_conf(pg.pg_id, pgcf)
    if not ok then
        err = err or "unknown error"
        return nil, string.format("loading pg(id:%d):%s. \
           raw json:%s",pg.pg_id, err,pgcf_str)
    end

    local policies = {}
    for _, policy in ipairs(pgcf.policies) do
        policies[policy.seq] = policy
    end

    pgcf.policies = sort_by_seq(policies)

    return pgcf
end

function load_app_conf(app)
    local key = string.format("ufe_%s",app)
    local acf_str = u.get(key)
    if not acf_str then
        return nil, "can not get app conf from remote"
    end

    local acf, err = cjson.decode(acf_str)
    if not acf then
        err = err or "unknown error"
        return nil, string.format("decode json failed:%s. \
            raw json:%s",err, acf_str)
    end

    local ok, err = check_ufe_app_conf(app, acf)
    if not ok then
        return nil, string.format("%s.raw json:%s",err, acf_str)
    end

    --get policy groups from remote
    local pgcfs = {}
    for _, pg in ipairs(acf.policy_groups) do
        local pgcf, err = load_pg_conf(pg)
        if not pgcf then
            return nil, err
        end
        pgcf.type = pg.type
        pgcf.seq = pg.seq
        pgcf.domains = pg.domains
        pgcf.uri = pg.uri
        pgcf.unit_sign = pg.unit_sign
        pgcfs[pg.seq] = pgcf
    end
    acf.policy_groups = sort_by_seq(pgcfs)
    acf.update_time = ngx.time()

    --set switch status no conditional, all invalid switch is off
    if  acf.ufe_switch == 'on' then
        shared.set_ufe_switch_locked(true, app)
    else
        shared.set_ufe_switch_locked(false, app)
    end

    return cjson.encode(acf)
end

local function load_all_conf(ufe_apps, apps_update_time)
    for _, app in ipairs(ufe_apps) do
        while true do
            if not shared.lock_app(app) then break end --some worker are updating this
            local app_str = shared.get_app_conf(app)
            if app_str then
                shared.unlock_app(app) -- already done, everything is ok, next
                break
            end

            local acf_str, err = load_app_conf(app)
            if not acf_str then
                err = err or "unknown error"
                load_conf_fail(app, err)
                shared.unlock_app(app)
                break
            end

            shared.set_app_conf(app, acf_str)
            ngx.log(ngx.INFO, string.format("loading app(%s) success:%s",app, acf_str))
            shared.unlock_app(app)
            break
        end
    end

    local loaded_all =  true
    --check whether all conf loaded done
    for _, app in ipairs(ufe_apps) do
        local acf_str  = shared.get_app_conf(app)
        if not acf_str then
            loaded_all = false
        end
    end

    --what if update apps here? no matter,because we will check apps' update_time
    return loaded_all
end

local function update_all_conf(premature)
    if premature then return false end
    updating_conf = true
    ngx.log(ngx.INFO,"[ufe] update all confs.")
    local now = ngx.time()
    -- only update all conf when ufe_apps is done
    if not shared.is_ufe_apps_done() then
        ngx.log(ngx.INFO,"ufe_apps is not done")
        ufe_confs = {last_check_time = now}
        updating_conf = false
        return false
    end

    local now = ngx.time()
    -- no apps, so no policy group
    local apps_str = shared.get_ufe_apps()
    if not apps_str then
        ngx.log(ngx.INFO,"there has no ufe apps in share dict.")
        ufe_confs = {last_check_time = now}
        shared.clear_loaded_conf()
        updating_conf = false
        return false
    end

    local apps, err = cjson.decode(apps_str)

    if not apps or not check_ufe_apps(apps) or not apps.update_time then
        -- there must be somethins wrong
        ngx.log(ngx.ERR,string.format("the apps in shared dict is wrong, unexpected! raw:%s",apps_str))
        ufe_confs = {last_check_time = now}
        shared.clear_loaded_conf()
        updating_conf = false
        return false
    end

    --we need to update conf per worker, clear all so we can delete some one
    if ufe_confs.update_time ~= apps.update_time then
        --still need to get app confs, if not done,we will try it again
        local done = load_all_conf(apps.ufe_apps, apps.update_time)

        if not done then
            local ok, err = ngx.timer.at(LOAD_APP_CONF_RETRY_TIME, update_all_conf)
            if not ok then
                updating_conf = false
                ngx.log(ngx.ERR, "cannot update confs. set timer failed:", err)
            end
        end

        -- set the apps that have been done
        -- first clear all so we can delete an app
        ufe_confs = {last_check_time = now}
        for _, app in ipairs(apps.ufe_apps) do
            local acf_str  = shared.get_app_conf(app)
            if  acf_str then
                local acf = cjson.decode(acf_str)
                if not acf or type(acf) ~= 'table' then
                    ngx.log(ngx.ERR,string.format("the app conf(%s) in shared dict is unexpected! %s",app,acf_str))
                else
                    ufe_confs[app] = acf
                end
            end
        end

        -- update per worker conf's update_time
        if done then
            updating_conf = false
            ufe_confs.update_time = apps.update_time
        end
        return done
    end
    updating_conf = false
    return false
end

local function update_all_conf_asyn()
    local ok, err = ngx.timer.at(0,update_all_conf)
    if not ok then
        ngx.log(ngx.ERR, "cannot update confs. set timer failed:", err)
    end
end

local function update_switch()
    switch = shared.get_switch()
end

function get_app_conf(app)
    local now = ngx.time()
    --check if need to update local conf
    --TODO:just check once
    if not updating_conf and now - ufe_confs.last_check_time >= CHECK_CONF_EXPIRED then
        update_all_conf_asyn() -- asynchronous
        ufe_confs.last_check_time = now
    end

    if now - switch.last_check_time >= CHECK_SWITCH_EXPIRED then
        update_switch() --synchronous
        switch.last_check_time = now
    end

    -- default switch is off
    if switch.global ~= true or switch[app] ~= true then
        --ngx.log(ngx.DEBUG,app," switch is off:",switch.global,switch[app],shared.get_switch_raw())
        return nil
    end

    return ufe_confs[app]

end

local function match(list, item)
    if not list or not item then return true end
    for _, l in ipairs(list) do
        if item == l then return true end
    end
    return false
end

function get_group(acf, app, host, uri)
    --go through every policy groups
    for _, pg in ipairs(acf.policy_groups) do
        --check if domain and uri match or cell match
        if match(pg.unit_sign,ngx.var.self_cell) and match(pg.domains, host) and match(pg.uri, uri) then
            local policies = pg.policies
            for _, p in ipairs(policies) do
                local key = acf.ufe_id .."-".. pg.id .. "-" .. p.id --for list action
                if action_handlers[p.action.name].execute(p.action, key, p.id) then
                    return pg.type, key
                end
            end
        end
    end

    --ngx.log(ngx.INFO,"policy not match")
    return nil
end
